/**
  ******************************************************************************
  * @author     Chris
  * @since      2024/6/25 13:24
  *
  * @file       hd_w25qxx.c
  * @brief      Xxx hardware driver.
  *
  * @note       This file contains the hardware driver for the Xxx.
  *
  * @warning    None.
  ******************************************************************************
  * Change Logs:
  *   Date          Author       Notes
  *   2024/6/25     Chris        the first version
  *
  ******************************************************************************
  */


#include <hd_w25qxx.h>
#include <stdlib.h>

extern void Driver_add(void *driver);

uint8_t *W25QXX_FLASH_BUFFER;

static void W25Qxx_init_(const W25Qxx *this) {
    uint8_t *buf = malloc(W25QXX_SECTOR_SIZE);
    Driver_add(buf);
    W25QXX_FLASH_BUFFER = buf;
}

static void W25Qxx_readID_(const W25Qxx *this, uint8_t *MID, uint16_t *DID) {
    SPI_start(this->spi, this->ssel);

    SPI_swapByte(this->spi, W25QXX_JEDEC_ID);
    *MID = SPI_swapByte(this->spi, W25QXX_DUMMY_BYTE);
    *DID = SPI_swapByte(this->spi, W25QXX_DUMMY_BYTE);
    *DID <<= 8;
    *DID |= SPI_swapByte(this->spi, W25QXX_DUMMY_BYTE);

    SPI_stop(this->spi, this->ssel);
}

static void W25Qxx_writeEnable(const W25Qxx *this) {
    SPI_start(this->spi, this->ssel);

    SPI_swapByte(this->spi, W25QXX_WRITE_ENABLE);

    SPI_stop(this->spi, this->ssel);
}

static void W25Qxx_waitBusy(const W25Qxx *this) {
    SPI_start(this->spi, this->ssel);

    SPI_swapByte(this->spi, W25QXX_READ_STATUS_REGISTER_1);

    uint32_t timeout = 100000;
    while (timeout--) {
        if ((SPI_swapByte(this->spi, W25QXX_DUMMY_BYTE) & 0x01) != 0x01) {
            break;
        }
    }

    SPI_stop(this->spi, this->ssel);
}

static void W25Qxx_read_(const W25Qxx *this, const uint32_t address, uint8_t *dat, uint32_t count) {
    SPI_start(this->spi, this->ssel);

    SPI_swapByte(this->spi, W25QXX_READ_DATA);

    SPI_swapByte(this->spi, address >> 16); // swap address[23:16]
    SPI_swapByte(this->spi, address >> 8);  // swap address[15:8]
    SPI_swapByte(this->spi, address);       // swap address[7:0]

    while (count--) {
        *dat++ = SPI_swapByte(this->spi, W25QXX_DUMMY_BYTE);
    }

    SPI_stop(this->spi, this->ssel);
}

static void W25Qxx_eraseSector_(const W25Qxx *this, const uint32_t address) {
    W25Qxx_writeEnable(this);

    SPI_start(this->spi, this->ssel);

    SPI_swapByte(this->spi, W25QXX_SECTOR_ERASE_4KB);

    SPI_swapByte(this->spi, address >> 16); // swap address[23:16]
    SPI_swapByte(this->spi, address >> 8);  // swap address[15:8]
    SPI_swapByte(this->spi, address);       // swap address[7:0]

    SPI_stop(this->spi, this->ssel);

    W25Qxx_waitBusy(this);
}

static void W25Qxx_writePage(const W25Qxx *this, const uint32_t address, const uint8_t *dat, uint16_t count) {
    W25Qxx_writeEnable(this);

    SPI_start(this->spi, this->ssel);

    SPI_swapByte(this->spi, W25QXX_PAGE_PROGRAM);

    SPI_swapByte(this->spi, address >> 16); // swap address[23:16]
    SPI_swapByte(this->spi, address >> 8);  // swap address[15:8]
    SPI_swapByte(this->spi, address);       // swap address[7:0]

    while (count--) {
        SPI_swapByte(this->spi, *dat++);
    }

    SPI_stop(this->spi, this->ssel);

    W25Qxx_waitBusy(this);
}

static void W25Qxx_writeNoCheck(const W25Qxx *this, uint32_t address, const uint8_t *dat, uint16_t count) {
    uint16_t pageRemain = W25QXX_PAGE_SIZE - address % W25QXX_PAGE_SIZE;

    if (count <= pageRemain) pageRemain = count;

    while (1) {
        W25Qxx_writePage(this, address, dat, pageRemain);
        if (count == pageRemain) break;

        dat += pageRemain;
        address += pageRemain;

        count -= pageRemain;
        pageRemain = count > W25QXX_PAGE_SIZE ? W25QXX_PAGE_SIZE : count;
    }
}

static void W25Qxx_write_(const W25Qxx *this, uint32_t address, const uint8_t *dat, uint16_t count) {
    uint16_t i;

    uint32_t sectorIndex = address / W25QXX_SECTOR_SIZE;       // sector index
    uint16_t sectorOffset = address % W25QXX_SECTOR_SIZE;      // offset of sector
    uint16_t sectorRemain = W25QXX_SECTOR_SIZE - sectorOffset; // remain + offset = W25QXX_SECTOR_SIZE

    if (count <= sectorRemain) sectorRemain = count; // offset + count <= W25QXX_SECTOR_SIZE

    while (1) {
        // read all data of this sector
        this->vptr->read(this, sectorIndex * W25QXX_SECTOR_SIZE, W25QXX_FLASH_BUFFER, W25QXX_SECTOR_SIZE);

        // check the sector erase or not
        for (i = 0; i < sectorRemain; ++i) {
            if (W25QXX_FLASH_BUFFER[sectorOffset + i] != 0xFF) break;
        }

        if (i < sectorRemain) {
            // must erase, and then copy the data to buffer, finally, write that all
            this->vptr->erase_sector(this, sectorIndex * W25QXX_SECTOR_SIZE);
            for (i = 0; i < sectorRemain; ++i) W25QXX_FLASH_BUFFER[i + sectorOffset] = dat[i];
            W25Qxx_writeNoCheck(this, sectorIndex * W25QXX_SECTOR_SIZE, W25QXX_FLASH_BUFFER, W25QXX_SECTOR_SIZE);
        } else {
            // should erase noting, and write remaining data
            W25Qxx_writeNoCheck(this, address, dat, sectorRemain);
        }

        if (count == sectorRemain) break; // write done, and exit

        sectorIndex++;
        sectorOffset = 0;
        dat += sectorRemain;
        address += sectorRemain;
        count -= sectorRemain;
        if (count > W25QXX_SECTOR_SIZE) sectorRemain = W25QXX_SECTOR_SIZE;
        else sectorRemain = count;
    }
}

static void W25Qxx_Storage_read(Storage *super, uint8_t *buffer, const uint32_t address, const uint32_t count) {
    const W25Qxx *this = (W25Qxx *) super;
    this->vptr->read(this, address, buffer, count);
}

void W25Qxx_constructor(W25Qxx *this, SPI *spi, GPIO *ssel) {
    static struct StorageVtbl vtbl = {
            .read = &W25Qxx_Storage_read
    };
    Storage_constructor(&this->super);
    this->super.vptr = &vtbl;

    static struct W25QxxVtbl w25QxxVtbl = {
            .init         = &W25Qxx_init_,
            .get_id       = &W25Qxx_readID_,
            .read         = &W25Qxx_read_,
            .erase_sector = &W25Qxx_eraseSector_,
            .write        = &W25Qxx_write_,
    };
    this->vptr = &w25QxxVtbl;
    this->spi  = spi;
    this->ssel = ssel;
}
